"""
缓存模式下的单例，在不同进程、同redis情况下生效
Example:
    class Singleton(BaseSingleton):
        def __init__(readonly: bool = False):
            self.readonly = readonly
            self.data: int = 0

    with Singleton() as obj:
        obj.data += 1
        obj.save()
Tips:
    重新运行时，redis存储的单例对象会置空
    使用pickle序列化，实例中不可被序列化的属性在__getstate__中删除
"""

import time
from django.conf import settings
import uuid
import pickle

from utils.commen.errors import CustomBaseError
from redis import Redis
from utils.database.redisConn import pool


class SingletonError(CustomBaseError):
    """
    """


class _CacheLock:
    """
    全局单例的读写锁，redis中存在键时则为锁定状态
    redis值存为当前锁的id
    """

    def __init__(self, singleton_name: str):
        # 单例类名拼接配置文件中设置的锁后缀
        self.name: str = singleton_name + settings.SINGLETON_LOCK_SUFFIX
        # 是否持有锁 True时不代表真的持有，不跟随redis缓存变化，False时必定不持有
        self.holder: bool = False
        # 锁唯一ID， 存入redis中
        self.id = str(uuid.uuid4())
        self.redis_conn: Redis = Redis(connection_pool=pool)

    def acquire(self, timeout: int = settings.SINGLETON_LOCK_EXPIRE) -> None:
        """
        加锁
        :param timeout: 超时时间 单位：秒
        :return:
        """
        # 阻塞获取锁
        for _ in range(int(timeout / settings.SINGLETON_LOCK_FREQUENCY)):
            # nx参数保证锁不存在才会加锁，保证判断和加锁操作的原子性
            if self.redis_conn.set(self.name, self.id, nx=True, ex=timeout):
                self.holder = True
                return
            time.sleep(settings.SINGLETON_LOCK_FREQUENCY)
        else:
            raise SingletonError('加锁超时')

    def release(self):
        """
        lua脚本中执行判断和释放操作，保证释放锁的原子性
        redis中和对象id一致时释放，否则视为未持有锁直接返回
        :return:
        """
        lua_script: str = """
if redis.call('GET', '%s') == '%s' then
    redis.call('DEL', '%s')
else
    return 0
end""" % (self.name, self.id, self.name)
        cmd = self.redis_conn.register_script(lua_script)
        cmd()
        self.holder = False

    def __enter__(self):
        self.acquire()
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.release()

        # 重新抛出异常
        if exc_type is not None:
            raise exc_type(exc_val)


class _SingletonMeta(type):
    """
    django单例元类, wsgi是多进程，所以使用redis实现单例
    """

    def __new__(cls, name, bases, attrs):
        """
        单例类定义时调用，生成缓存，初始值为None对象，并生成单例锁
        :param name:
        :param bases:
        :param attrs:
        """
        _obj = super().__new__(cls, name, bases, attrs)

        # 基类不生成数据和锁
        if name == 'BaseSingleton':
            return _obj

        _r = Redis(connection_pool=pool)
        # 生成空数据
        _r.set(name, pickle.dumps(None), None)

        return _obj

    def __call__(cls: type, *args, **kwargs):
        """
        生成对象前调用，单例模式核心
        检测关键字中是否有readonly字段
        :param args:
        :param kwargs:
        :return:
        """
        if len(args) > 0 or len(kwargs) > 1 or (len(kwargs) == 1 and kwargs.get('readonly', False)):
            raise SingletonError('单例对象实例化请勿传参')
        if cls.__name__ == 'BaseSingleton':
            raise SingletonError('单例基类不可实例化')
        _r = Redis(connection_pool=pool)

        # 只读模式不加锁直接返回
        if kwargs.get('readonly', False):
            _obj: "BaseSingleton" = pickle.loads(_r.get(cls.__name__))
            if _obj is None:
                raise SingletonError(f'{cls.__name__} 未初始化')
            return _obj

        lock = _CacheLock(cls.__name__)
        lock.acquire()

        _obj = pickle.loads(_r.get(cls.__name__))
        if _obj is None:
            # 生成对象唯一ID
            instance_id: str = str(uuid.uuid4())
            _obj = super().__call__(*args, **kwargs)
            _obj._instance_id = instance_id

            _obj._lock = lock
            _obj.save()
        else:
            _obj._lock = lock
        return _obj


class BaseSingleton(metaclass=_SingletonMeta):
    """
    单例模式基类
    创建对象时者关键字参数readonly设置是否只读，只读模式不加锁，直接读取当前数据

    Example:
        with BaseSingleton() as singleton:
            singleton.data += 1
            singleton.save()
    可以使用with语法防止抛出异常导致无法调用对象的__del__方法，从而导致不释放锁
    未使用with语法时出现程序异常则不会释放，直至锁自动消失
    """

    def __init__(self, readonly: bool = False):
        """
        单例对象只接受一个关键字传参
        :param readonly: 只读模式不加锁，该参数必须在创建对象时显式传入，不传默认为False
        """
        self.readonly = readonly
        self._lock: _CacheLock

    @classmethod
    def class_name(cls) -> str:
        """
        返回类名
        :return:
        """
        return cls.__name__.__str__()

    @property
    def instance_id(self) -> str:
        """
        单例对象唯一ID
        :return:
        """
        return self.__getattribute__('_instance_id')

    @property
    def _locking(self) -> bool:
        ref: _CacheLock = self.__getattribute__('_lock')
        return ref.holder

    def save(self):
        """
        用lua脚本执行redis判断锁和更新对象数据，保证save操作的原子性

        :return:
        """
        if self.readonly:
            raise SingletonError('只读模式不可保存')
        if not self._locking:
            raise SingletonError('锁已释放，无法保存')

        ref: _CacheLock = self.__getattribute__('_lock')
        # lua脚本
        lua_script: str = """
if redis.call('GET', '%s') == '%s' then
    return redis.call('SET', '%s', ARGV[1])
else
    return 0
end""" % (ref.name, ref.id, self.class_name())

        cmd = ref.redis_conn.register_script(lua_script)
        result = cmd(args=[pickle.dumps(self)])
        if not result:
            ref.holder = False
            raise SingletonError(f'锁已失效 保存失败')

    def delete(self):
        """
        对象置空，新建对象时重新生成
        lua脚本执行redis命令 保证原子性
        :return:
        """
        if self.readonly:
            raise SingletonError('只读模式不可删除')
        if not self._locking:
            raise SingletonError('锁已释放，无法删除')

        ref: _CacheLock = self.__getattribute__('_lock')
        # lua脚本
        lua_script: str = """
if redis.call('GET', '%s') == '%s' then
   return redis.call('SET', '%s', ARGV[1])
else
    return 0
end""" % (ref.name, ref.id, self.class_name())

        cmd = ref.redis_conn.register_script(lua_script)
        result = cmd(args=[pickle.dumps(self)])
        if not result:
            ref.holder = False
            raise SingletonError(f'锁已失效 删除失败')

    def __setattr__(self, key, value):
        """
        防止修改对象唯一ID
        :param key:
        :param value:
        :return:
        """
        if key == '_instance_id' and self.__dict__.get('_instance_id') is not None:
            raise SingletonError("单例类的_instance_id属性不可更改")
        super().__setattr__(key, value)

    def __del__(self):
        """
        对象删除时释放锁
        :return:
        """
        ref: _CacheLock = self.__getattribute__('_lock')
        if ref.holder:
            ref.release()

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        """
        with语法退出时释放锁
        :param exc_type: 异常类型
        :param exc_val: 异常对象
        :param exc_tb: 异常的traceback
        :return:
        """
        ref: _CacheLock = self.__getattribute__('_lock')
        if ref.holder:
            ref.release()
        # 重新抛出异常
        if exc_type is not None:
            raise exc_val

    def __getstate__(self):
        """
        pickle序列化时不序列化锁对象
        :return:
        """
        state = self.__dict__.copy()
        del state['_lock']
        return state
